Skip to content

docs(plugin-charts): mark normalizeSeries' chartType limb internal-only and pin the split - #7815

Merged
os-sam merged 1 commit into
mainfrom
claude/issue-7744-charttype-internal-only
Sep 5, 2026
Merged

docs(plugin-charts): mark normalizeSeries' chartType limb internal-only and pin the split#7815
os-sam merged 1 commit into
mainfrom
claude/issue-7744-charttype-internal-only

Conversation

@claude

@claude claude Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Fixes #7744

Option B of the card's three-way disposition, per the PM ruling on the claim comment: the
chartType limb is annotated as internal-only and the split is pinned. Not A (leave the two
faces disagreeing with nothing written down), and not C — retiring the limb at the reader
would change what the internal producers render, which is a reader-side decision of its own.

No behaviour changes. normalizeSeries returns byte-identical output for the same input;
the diff is a docblock, a two-line comment beside the limb, one new test file, and an
empty-frontmatter changeset. ChartRenderer.tsx and packages/types/ are untouched.

What the split is

normalizeSeries resolves the per-series family as str(raw.chartType) ?? str(raw.type) — the
internal spelling first. That limb is correct: chartType is the carrier of the renderer's
internal dataKey series shape. ChartRenderer's series? prop is a union of exactly the two
shapes, and it declares chartType? on the dataKey arm and on no other — the authored arm
carries type? instead. DashboardRenderer, ObjectView and the dataset path write that arm.

But the one function normalizes both shapes, and by the time raw arrives it is a bag of keys
with nothing on it naming its producer. So an author who skips validation still gets chartType
honoured at runtime — over the declared type, read second — while the authoring face refuses
that same key by name. Two faces, one disagreement, and until now nothing anywhere said so.

1. The annotation, and the refusal it delegates to

The normalizeSeries docblock now names which face owns which spelling, records that the limb is
lit (and that a green ablation on it is not a licence to delete it), and states that the split is
deliberate and pinned.

It delegates rather than summarises: the refusal is named as
ChartDataSeriesSchema.chartType in packages/types/src/zod/data-display.zod.ts
(objectui#7694 / PR #7737), together with its existing pin
packages/types/src/__tests__/chart-series-chart-type-alias-refusal-7694.test.ts. The rule, its
message and its remedy are not copied into plugin-charts — per objectui#7765's landing record,
a one-line summary in a second file rots, because only one of the two files is ever edited.

2. The pin, and why this shape catches drift

packages/plugin-charts/src/__tests__/chartType-internal-only-7744.test.ts.

The refusal already had a pin, and the reader already had pins. Neither file pinned the pair
which is exactly how the split survived to be found in the contract review of PR #7737. What this
case pins is the relationship: the key the validator refuses is the key the reader prefers.

Shape choices that make it catch drift rather than describe it:

  • One fixture, read by every assertion{ name: 'margin', type: 'area', chartType: 'line' }
    so the two faces are measured against the same input, not against two fixtures free to drift.
  • Both spellings written, with different values. This is what makes the reader's precedence
    observable at all. With only chartType written, a reordered reader returns the identical answer
    and a pin on the value alone stays green straight through the change it exists to catch.
  • The refusal side asserts the issue paths equal exactly ['chartType'] — so the fixture being
    refused for some other reason fails too, rather than passing as a phantom. An accept-control on
    the same document with the key dropped is the other half of that.
  • Nothing loose. No assertion is written as "chartType is honoured somehow"; each one fails
    under exactly one of the two behaviours, and the messages name objectui#7744 and say which side
    moved ("area" means REORDERED, undefined means DELETED).
  • A compile-time half over ChartRenderer's series? union arms pins the same split in
    TypeScript. Confirmed compiled, not phantom: tsc -p tsconfig.test.json --listFiles resolves the
    file (1 hit, not 0).

3. Proof of no behaviour change (same input, same output)

A probe imports the module and runs 336 input cases through normalizeChartSchema — the only
public entry into normalizeSeries — plus comboBaseFamily, effectiveChartFamily,
formatterFor, ticksFor, domainFor and the exported sets. 288 of those cases are the
chartType/type limb exhaustively: every combination of present/absent across 12 values per
spelling, including disagreeing pairs, empty strings, wrong-typed values, and the same matrix again
reached through categories.

before (blob 05a66dfb…)  sha256 1681dfd534b7f01f798bc9fef45368abba0051bc136c67e3b7cd0c410c7fd896
after  (blob 0a2b702c…)  sha256 1681dfd534b7f01f798bc9fef45368abba0051bc136c67e3b7cd0c410c7fd896
=> byte-identical

The pre-edit blob is 05a66dfb5b9c9a322761e1e1a694ac7a57af1e54 — the same blob the card names at
both ends of PR #7737, so this is measured against exactly the reader the reviewer measured.

The probe was itself shown sensitive, otherwise "identical" would be vacuous: reordering the
limb in a scratch copy (never in the repo) moves 49 of the 336 cases, e.g.
{ name: 'margin', chartType: 'bar', type: 'line' } goes from chartType: 'bar' to 'line'.

The stronger reading agrees: the package's pre-existing suite was 45 files / 410 tests, all
passing
before the edit and 46 files / 416 tests, all passing after — the delta is exactly
this PR's one new file and its six cases; no pre-existing test changed verdict.

4. Lit controls — the pin goes red from either side, and names it

Both mutations ran under trap … EXIT INT TERM with absolute paths, the mutation confirmed on disk
by anchored text counts and a changed blob hash before anything was read (an empty hash reads as
failure), and restoration proved by blob-hash equality plus an empty git diff HEAD — state,
not whether the trap fired. Both were run after the implementation was committed, so HEAD is
the correct restore reference.

mutation result which cases go red
reader: str(raw.chartType) ?? str(raw.type) reordered 2 failed, 4 passed (exit 1) "the READER honours chartType…" and "THE SPLIT…"
authoring face: ChartDataSeriesSchema.chartType made to accept 2 failed, 4 passed (exit 1) "the AUTHORING face refuses chartType…" and "THE SPLIT…"

The relationship case is red in both directions; the side-specific cases name which face
moved. Sample diagnostic, verbatim from the reader mutation:

AssertionError: objectui#7744: `normalizeSeries` no longer resolves the per-series family from
`chartType` first. `"area"` here means the limb was REORDERED; `undefined` means it was DELETED.
Either one changes what the internal `dataKey`-shape producers render (DashboardRenderer /
ObjectView / the dataset path) — that is a reader-side decision, not a doc fix.
expected 'area' to be 'line'

Restoration, both controls:

reader    HEAD blob 0a2b702c…  mutated 1f53c339…  post 0a2b702c…  git diff HEAD: empty
authoring HEAD blob 1cbacf06…  mutated ae8318b0…  post 1cbacf06…  git diff HEAD: empty
whole tree afterwards: git status --short empty; the pin re-runs 6 passed, exit 0

5. Verification

Union run after the final commit, at 1120ee392, uncached (--force), through turbo:

command exit result
pnpm turbo run test type-check --filter=@object-ui/plugin-charts --force 0 Test Files 46 passed (46) · Tests 416 passed (416) · Tasks: 11 successful, 11 total
pnpm --filter @object-ui/plugin-charts lint 0 0 errors, 334 pre-existing warnings; the new file and the edited file contribute 0 errors / 0 new warnings

Derived gate family for the changed paths (.changeset/*.md, packages/plugin-charts/src/**), all
exit 0: check-control-bytes · check-changeset-no-major · check-changeset-overwrite ·
check-changeset-fixed · check-lint-coverage · check-type-check-coverage ·
check-unreferenced-sources. Also self-scanned for control bytes beyond the gate
(grep -naP over both touched files, no hits).

Declared narrowing: the repo-wide pnpm lint was not run locally; eslint was run on the
affected package only. Evidence the narrowing excludes nothing: the population is read from
eslint's own config, not guessed (57 files resolved in this package, from --format json); this
repo has no type-aware linting configured (no projectService / parserOptions.project in
eslint.config.*), so a comment-only edit cannot move any untouched file's verdict. CI runs the
full farm regardless.

6. Changeset gate — verdict verbatim

node scripts/check-changeset-presence.mjs, exit 0:

Compared the working tree with a617bb8d3 (merge-base with origin/main): 3 file(s) changed, 2 of
them published source of a package the release covers, 0 of them a manifest whose published
contract moved, 0 under a package changesets ignores, 1 changeset(s) added.
✅  2 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s):
.changeset/7744-chart-type-internal-only.md.
    Every one of them has an EMPTY frontmatter — declared as releasing nothing, which
    is the explicit exemption and a complete answer to this gate.

The empty-frontmatter form is the honest one here: B ships no published behaviour. Never major.
No skip-changeset label was applied — in this repo that label reads nothing and exempts nothing;
the empty-frontmatter declaration is the mechanism, and the gate's verdict line is the authority.

Clause-② remains no

Nothing here makes normalizeSeries produce a different output for the same input — that is
measured above, not asserted. The limb is untouched (still 1 occurrence, unchanged);
ChartRenderer.tsx and packages/types/ carry no edit in this PR.

🤖 Generated with Claude Code

https://claude.ai/code/session_01KbJQ1y1J12nZxYzFWhP8Q3


Generated by Claude Code

…only and pin the split (objectui#7744)

`normalizeSeries` resolves the per-series family as
`str(raw.chartType) ?? str(raw.type)` — the INTERNAL spelling first. That limb is
correct and lit: `chartType` is the carrier of the renderer's internal `dataKey`
series shape, which `ChartRenderer`'s `series?` union declares on that arm and on
no other, and which `DashboardRenderer` / `ObjectView` / the dataset path write.

But the same function normalizes authored spec-shape series too, and by the time
`raw` arrives it is a bag of keys with nothing on it naming its producer. So an
author who skips validation still gets `chartType` honoured at runtime — over the
declared `type`, read second — while the authoring face refuses that same key by
name (`ChartDataSeriesSchema`, objectui#7694 / PR #7737). The two faces disagree,
and nothing said so anywhere.

This annotates and pins that split rather than retiring the limb: retiring it
would change what the internal producers render, a reader-side decision of its
own. No behaviour changes — `normalizeSeries` returns byte-identical output for
336 input cases before and after, and the probe is sensitive (a reader reorder
moves 49 of them).

- the `normalizeSeries` docblock names which face owns which spelling, DELEGATES
  the refusal's rule and remedy to `ChartDataSeriesSchema` rather than copying
  them, and records why a green ablation on the first limb is not a licence to
  delete it;
- `chartType-internal-only-7744.test.ts` pins the RELATIONSHIP the two existing
  files each held only half of: the key the validator refuses is the key the
  reader prefers. It reads one fixture writing BOTH spellings with DIFFERENT
  values, so a reorder is observable and not just a deletion; and it carries a
  compile-time half over `ChartRenderer`'s `series?` union arms.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KbJQ1y1J12nZxYzFWhP8Q3
@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 50 chunks) 3187.4 KB 3191.4 KB
Main entry chunk (gzip) 143.2 KB 350 KB
Entry file index-CnaboxA9.js
Status PASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 15.67KB 5.75KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 5.13KB 2.35KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 510.60KB 116.20KB
core (index.js) 6.96KB 2.79KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 182.08KB 50.62KB
fields (index.js) 242.44KB 61.25KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 4.28KB 1.75KB
i18n (index.js) 3.65KB 1.47KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 26.89KB 9.04KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.98KB 10.98KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 4.39KB 1.66KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 11.71KB 4.29KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 5.12KB 1.74KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 15.75KB 3.80KB
plugin-calendar (index.js) 47.87KB 13.31KB
plugin-charts (index.js) 70.92KB 19.75KB
plugin-chatbot (index.js) 196.19KB 46.37KB
plugin-dashboard (index.js) 132.88KB 34.69KB
plugin-designer (index.js) 212.86KB 43.19KB
plugin-detail (index.js) 250.55KB 64.06KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 132.87KB 32.66KB
plugin-gantt (index.js) 167.26KB 41.00KB
plugin-grid (index.js) 209.29KB 56.78KB
plugin-kanban (index.js) 52.71KB 14.55KB
plugin-list (index.js) 113.28KB 27.59KB
plugin-map (index.js) 20.44KB 6.78KB
plugin-markdown (index.js) 13.93KB 4.81KB
plugin-report (index.js) 43.59KB 11.97KB
plugin-timeline (index.js) 30.84KB 8.85KB
plugin-tree (index.js) 9.20KB 3.19KB
plugin-view (index.js) 85.24KB 20.94KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 81.07KB 26.86KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 2.32KB 1.24KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 5.41KB 2.34KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 4.93KB 2.24KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 20.57KB 5.88KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 10.35KB 3.60KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.74KB 1.41KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 4.73KB 2.28KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-sam
os-sam marked this pull request as ready for review September 5, 2026 23:16
@os-sam
os-sam added this pull request to the merge queue Sep 5, 2026
Merged via the queue into main with commit 3faaa7d Sep 5, 2026
34 checks passed
@os-sam
os-sam deleted the claude/issue-7744-charttype-internal-only branch September 5, 2026 23:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

finding(plugin-charts): normalizeSeries reads chartType FIRST on the authored path while the validator now refuses it by name — the two faces disagree

2 participants